-----------------Zork I----------------
A 4am crack                  2018-03-19
---------------------------------------

Name: Zork I
Version: release 5
Genre: adventure
Year: 1980
Credits: Infocom
Publisher: Personal Software, Inc.
Platform: Apple ][ or later (32K)
Media: 5.25-inch disk
Sides: 1
OS: custom
Previous cracks: none

                   ~

               Chapter 0
 In Which Various Automated Tools Fail
          In Interesting Ways
 Because The Steady March of Progress
      Is A Real Kick In The Pants


First things first: this is a 13-sector
disk. As in, it will not even boot on
"modern" 16-sector floppy drives -- the
ones Apple introduced in 1980. There
was a transition period in the early
1980s where software developers put two
bootloaders on track 0, one for the old
13-sector drives and another for the
"new" 16-sector drives.

This disk does not have that. It's 13
sectors or bust.

If you have a "new" 16-sector drive,
you can boot the game by booting the
DOS 3.3 System Master disk and typing

]BRUN BOOT13

then putting the original disk in the
drive.

All my automated tools are useless.
Passport can trace 13-sector disks --
but only if it contains the 16-sector
shim bootloader. COPYA can only copy
16-sector disks. Same with Locksmith
Fast Disk Backup. Even my favorite
sector editor, Disk Fixer, has no
capability to read 13-sector disks.

Time to get some new (old) tools.

Next steps:

  1. Find tools that can read and/or
     copy 13-sector disks
  2. ???

                   ~

               Chapter 1
  In Which We Find Some New Old Tools


Not everything I have is useless, but I
get to start using them differently.
Without straying too far from the
familiar, I started with Copy II Plus.
Version 5.5 was the last version to
support 13-sector disks in the main
utilities. (Later versions could still
bit copy them, of course.) Selecting
"NEW DISK INFO" and changing Slot 6,
Drive 1 from "DOS 3.3" to "DOS 3.2"
will tell Copy II Plus that there is a
13-sector disk in that drive.

Selecting "VERIFY DISK" successfully
reads and verifies track 0 of the
original disk, then starts giving read
errors on every sector of every track.
Then, surprisingly, verifies track $1D
through $22 without complaint. Perhaps
those higher tracks are unused?

                 --v--

VERIFY DISK                      DISK A


ERROR TRACK $17
SECTOR $0 1 2 3 4 5 6 7 8 9 A B C

ERROR TRACK $18
SECTOR $0 1 2 3 4 5 6 7 8 9 A B C

ERROR TRACK $19
SECTOR $0 1 2 3 4 5 6 7 8 9 A B C

ERROR TRACK $1A
SECTOR $0 1 2 3 4 5 6 7 8 9 A B C

ERROR TRACK $1B
SECTOR $0 1 2 3 4 5 6 7 8 9 A B C

ERROR TRACK $1C
SECTOR $0 1 2 3 4 5 6 7 8 9 A B C

TOTAL : 364 ERRORS
PRESS [RETURN]

                 --^--

OK, so tracks $01-$1C are protected. If
track $00 is read by the drive firmware
and tracks $1D-$22 are unused, then I'm
dealing with one structural protection
covering all the game assets.

Copy II Plus also has a sector editor.
In version 5.5, it's part of the main
utilities. (Later versions bumped it to
the bit copy utility.) If I can verify
track $00, maybe I can inspect it too?

Indeed, the sector editor gets me in
the front door. I can read individual
sectors; I can list disassembled code.
Here is the code on track 0, sector 1:

                 --v--

; RWTS call?
0900-   A9 23       LDA   #$23
0902-   A0 C0       LDY   #$C0
0904-   20 00 29    JSR   $2900

; try forever until success
0907-   B0 F7       BCS   $0900

; increment address and sector
0909-   EE C9 23    INC   $23C9
090C-   EE C5 23    INC   $23C5

; 13 sectors per track
090F-   AD C5 23    LDA   $23C5
0912-   C9 0D       CMP   #$0D
0914-   D0 EA       BNE   $0900

; reset sector and increment track
0916-   A9 00       LDA   #$00
0918-   8D C5 23    STA   $23C5
091B-   EE C4 23    INC   $23C4

; up to (but not including) track 3
091E-   AD C4 23    LDA   $23C4
0921-   C9 03       CMP   #$03
0923-   D0 DB       BNE   $0900

; set some zero page addresses (not
; sure what these mean yet)
0925-   A9 60       LDA   #$60
0927-   85 7C       STA   $7C
0929-   A9 0D       LDA   #$0D
092B-   85 7F       STA   $7F
092D-   A9 1F       LDA   #$1F
092F-   85 7B       STA   $7B

; initialize text and input vectors
0931-   20 2F FB    JSR   $FB2F
0934-   20 93 FE    JSR   $FE93
0937-   20 89 FE    JSR   $FE89

; jump to code we just read
093A-   4C 00 08    JMP   $0800

                 -^--

This sector editor always disassembles
code as if it started at $0900, but I'm
fairly sure this code is really loaded
at $2300. The first three instructions
appear to be calling a DOS-shaped RWTS
entry point at $2900, and there is an
RWTS parameter table at offset $C0 of
this sector, the address of which it is
passing in A and Y.

                 --v--

C0- 01 60 01 00 01 00 D1 23  .`....Q#
                ^^^^^
           track 1, sector 0

C8- 00 08 00 00 01 00 00 60  .......`
    ^^^^^
address $0800

D0- 01 00 01 EF D8 00 00 00  ...oX...

                 --^--

So we're reading all of track 1 and 2
into $0800+, which, at 13 sectors per
track, works out to $0800..$20FF.

Further investigation with Copy II Plus
bit copy confirms that it can produce a
working copy on another floppy disk.
Whatever protection is preventing the
main Copy II Plus utilities from
verifying tracks $01-$1C, it doesn't
fool the bit copier.

This is excellent news. The combination
of a working copy and a sector editor
that can write to that working copy,
means I can remove the original disk
from the equation and start hacking up
this code directly.

                   ~

               Chapter 2
In Which We Hack Up This Code Directly


The first change I want to make is to
break into the monitor instead of
executing this code. Thus -- ON A COPY,
NOT THE ORIGINAL THAT SELLS FOR $1200
ON EBAY -- I used the Copy II Plus 5.5
sector editor to make this small patch:

T00,S01,$00: A923A0 -> 4C59FF

Booting my hacked copy (through BOOT13,
like the original disk) successfully
drops me into the monitor with the boot
code and protected RWTS in memory.

*2300L

2300-   4C 59 FF    JMP   $FF59

*2900L

2900-   84 48       STY   $48
2902-   85 49       STA   $49
2904-   A0 02       LDY   #$02
2906-   8C F8 06    STY   $06F8
2909-   A0 04       LDY   #$04
290B-   8C F8 04    STY   $04F8

...and so forth.

I believe this RWTS is capable of
reading the protected tracks $01-$1C.
Let's save it to a file and find out.

[S5,D1=my work disk]

*C500G
...

]BSAVE OBJ.2200-2BFF,A$200,L$A00

                   ~

               Chapter 3
  In Which We Attempt To Use The Disk
      As A Weapon Against Itself


Advanced Demuffin is a cracker's tool
to convert disks to a standard format.
It takes a copy of the original disk's
RWTS (which you must supply), uses that
to read the original, while using its
own copy of a standard RWTS to write
out a copy in a standard format, sector
by sector.

I've included the latest version of
Advanced Demuffin on my work disk.

]BLOAD ADVANCED DEMUFFIN 1.5

By a fortuitous coincidence, Advanced
Demuffin and this protected RWTS do not
overlap each other in memory. But I do
need to make one small adjustment: by
default, Advanced Demuffin will store
sector data at $2000-$8FFF, which would
obliterate the protected RWTS at $2400
and crash after reading a few sectors.

Thus:

]CALL -151

*1CF0:40    ; save sector data at $4000
            ; (instead of $2000)

See the Advanced Demuffin documentation
for details on runtime parameters.
https://
archive.org/details/AdvancedDemuffin15

With that tweak in place, I can start
the conversion:

*800G       ; launch Advanced Demuffin

[press "C" to convert disk]

["Y" to change default values]

                 --v--

ADVANCED DEMUFFIN 1.5    (C) 1983, 2014
ORIGINAL BY THE STACK    UPDATES BY 4AM
=======================================


INPUT ALL VALUES IN HEX


SECTORS PER TRACK? (13/16) 13       <--

START TRACK: $01                    <--
START SECTOR: $00
END TRACK: $1C                      <--
END SECTOR: $0C                     <--

INCREMENT: 1

MAX # OF RETRIES: 0

COPY FROM DRIVE 1
TO DRIVE: 2
=======================================
13SC $01,$00-$1C,$0C BY1.0 S6,D1->S6,D2

                 --^--

[S6,D1=original disk]
[S6,D2=blank disk]

And here we go...

                 --v--

ADVANCED DEMUFFIN 1.5    (C) 1983, 2014
ORIGINAL BY THE STACK    UPDATES BY 4AM
=======PRESS ANY KEY TO CONTINUE=======
TRK: ............................
+.5:
    0123456789ABCDEF0123456789ABCDEF012
SC0: ............................
SC1: ............................
SC2: ............................
SC3: ............................
SC4: ............................
SC5: ............................
SC6: ............................
SC7: ............................
SC8: ............................
SC9: ............................
SCA: ............................
SCB: ............................
SCC: ............................
SCD:
SCE:
SCF:
=======================================
13SC $01,$00-$1C,$0C BY1.0 S6,D1->S6,D2

                 --^--

This is the power and the genius of
Advanced Demuffin. Every disk must be
able to read itself. So, let it read
itself, then capture the data and write
it out in a standard format.

Now what?

                   ~

               Chapter 4
             Franken-Zork


I have, or think I have, all the game
code and data on tracks $01-$1C of an
unprotected 16-sector disk. (The last
three sectors of each track are blank.)
This is not the most convenient format,
but it's progress.

The only thing I'm missing is track 0.
I can't simply copy the code from the
original disk, because the RWTS is for
a 13-sector disk. It's not a matter of
patching some prologue or epilogue
values. The entire process of decoding
disk nibbles to memory bytes is
radically different.

Although...

DOS 3.2 (13-sector) and DOS 3.3 (16-
sector) are remarkably similar, on
purpose. Pretty much the only thing
that changed between the last version
of 3.2 and the first version of 3.3 was
the RWTS. Furthermore, Apple kept the
entry points and calling convention the
same. You can create a "Franken-disk"
that contains the DOS 3.2 OS code but
the DOS 3.3 RWTS. (Passport does this
to auto-convert protected 13-sector
disks.)

Infocom released later versions of Zork
as protected 16-sector disks. If they,
like Apple, swapped in 16-sector RWTS
code but kept the calling convention
the same, maybe I can do the same?

I have previously cracked Zork I r15
(crack #1459), which I believe was the
first version of Zork I that Infocom
released under their own name. Let's
take a look at that disk's bootloader.

[S6,D1=Zork I r15]

[back to my favorite sector editor,
 because there ain't no memory like
 muscle memory]

Track 0, sector 0 looks like a DOS 3.3
boot sector, but it loads at $2200
instead of $B600. That's promising!

Sector 1 looks like this:

                 --v--

T00,S01
----------- DISASSEMBLY MODE ----------
0000:A9 1F          LDA   #$1F
0002:85 7B          STA   $7B

; call RWTS
0004:A9 23          LDA   #$23
0006:A0 C0          LDY   #$C0
0008:20 00 29       JSR   $2900

; try forever until success
000B:B0 F7          BCS   $0004

; increment address
000D:EE 43 23       INC   $2343

; until a sector counter
0010:AD 43 23       LDA   $2343
0013:C9 1A          CMP   #$1A
0015:F0 18          BEQ   $002F

; increment address and sector
0017:EE C9 23       INC   $23C9
001A:EE C5 23       INC   $23C5

; 16 sectors per track
001D:AD C5 23       LDA   $23C5
0020:C9 10          CMP   #$10
0022:D0 E0          BNE   $0004

; reset sector and increment track
0024:A9 00          LDA   #$00
0026:8D C5 23       STA   $23C5
0029:EE C4 23       INC   $23C4
002C:4C 04 23       JMP   $2304

; Execution continues here from the BEQ
; at $2315, once the sector counter
; hits $1A.
; Set some more zero page addresses
002F:A9 60          LDA   #$60
0031:85 7C          STA   $7C

; This, in particular, was #$0D on the
; original 13-sector disk. Perhaps they
; built in the number of sectors per
; track into their interpreter? Could I
; really be that lucky?
0033:A9 10          LDA   #$10
0035:85 7F          STA   $7F

; same machine stuff
0037:20 2F FB       JSR   $FB2F
003A:20 93 FE       JSR   $FE93
003D:20 89 FE       JSR   $FE89

; and jump to the code we just read
0040:4C 00 08       JMP   $0800

                 --^--

Other than using a counter to control
how many sectors to read, this
bootloader is almost identical to the
original 13-sector version. (The
original 13-sector disk read all of
tracks $01 and $02.) More importantly,
it appears that Infocom did do exactly
what I had hoped they would do: swap
out their 13-sector RWTS with a 16-
sector RWTS in exactly the same memory
range, $2400..$2BFF.

Which means it's time for me to make a
Franken-Zork.

[S6,D1=Zork r15 (16-sector bootloader)]
[S6,D2=my work-in-progress Zork r5]

[Copy II Plus]
  [MANUAL SECTOR COPY]
    [Source: Slot 6, Drive 1]
    [Target: Slot 6, Drive 2]
    [Tracks: 0 only]
    [GO]

Great. Now I have the r5 game code on
tracks $01-$1C (sectors $00-$0C of each
track), the r15 bootloader on track 0,
and no idea if it's going to work.

Ah, wait. Because the code on tracks
$01-$02 is only stored on the first 13
sectors of each track, I actually want
the original code on track 0, sector 1.
Except for zero page $7F, which (if my
hunch is correct), is a parameter to
tell the Infocom interpreter that the
disk has 13 sectors of usable data on
each track.

Thus, my final boot code on T00,S01
looks exactly like the original 13-
sector disk. I'd like to tell you I had
some fancy way of transferring it, but
in reality I typed it out by hand and
made a stupid typo which I will elide
over for the purposes of this write-up.

Franken-Zork indeed.

]PR#6
...crashes at $1DF1...

Well, foo.

                   ~

               Chapter 5
       Franken-Zork Strikes Back


Investigating in the monitor at the
point of the crash, it looks like at
least some of the code from tracks $01-
$02 is being loaded. Which is great.
$0800, for instance, looks like this:

1DF1-    A=2B X=00 Y=7F P=31 S=F9

*800L

0800-   D8          CLD
0801-   A9 00       LDA   #$00
0803-   A2 80       LDX   #$80
0805-   95 00       STA   $00,X
0807-   E8          INX
0808-   D0 FB       BNE   $0805
080A-   A2 FF       LDX   #$FF
080C-   9A          TXS

...which is stored on T01,S00. So we're
definitely loading interpreter from
tracks $01-$02.

$0900..$09FF, however, is all zeroes.
Which is not great.

$0A00 has code, but it's the wrong code
(or rather, code that belongs somewhere
else):

*A00L

0A00-   4C A7 09    JMP   $09A7
0A03-   20 09 1D    JSR   $1D09
0A06-   20 97 15    JSR   $1597
0A09-   18          CLC
0A0A-   A5 82       LDA   $82
0A0C-   65 BA       ADC   $BA
0A0E-   85 82       STA   $82

Returning to my trusty sector editor,
that code is from T01,S0B. It should
have been loaded into $1300..$13FF.

Aha! This is a sector ordering problem.

Looking at the RWTS I copied from the
16-sector Zork r15, I confirm this
suspicion:

                 --v--

-------------- DISK EDIT --------------
TRACK $00/SECTOR $09/VOLUME $FE/BYTE$B8
---------------------------------------
$A8: FF FF FF FF FF FF FF FF   ........
$B0: FF FF FF FF FF FF FF FF   ........
$B8:>00<04 08 0C 01 05 09 0D   @DHLAEIM
$C0: 02 06 0A 0E 03 07 0B 0F   BFJNCGKO
$C8: 20 93 FE AD 81 C0 AD 81    .~-.@-.
$D0: C0 A9 00 8D 00 E0 4C 44   @)@.@`LD

                 --^--

The 16-byte array at offset $B8 is the
mapping between physical and logical
sectors. Standard 16-sector disks,
including my work-in-progress Franken-
Zork, use a different mapping:

                 --v--

-------------- DISK EDIT --------------
TRACK $00/SECTOR $09/VOLUME $FE/BYTE$B8
---------------------------------------
$B8:>00<0D 0B 09 07 05 03 01   @MKIGECA
$C0: 0E 0C 0A 08 06 04 02 0F   NLJHFDBO

                 --^--

I made that change to my work-in-
progress Franken-Zork and rebooted.

T00,S09,$B8:
  0004080C0105090D02060A0E03070B0F ->
  000D0B09070503010E0C0A080604020F

]PR#6
...boots just as far, but hangs instead
   of crashing...

I am not at all sure this is progress.

                   ~

               Chapter 6
 In Which Optimizations Have A Way Of
Coming Back To Bite You 38 Years Later


Pressing <Ctrl-Reset> gets me to the
monitor. Checking $0800+, it appears
that the sector map is now correct. The
code stored on T01,S00 is at $0800; the
code on T01,S01 is at $0900; and so on.
So that is definitely progress.

So what's going on now? To answer that
question, I delved into the interpreter
code itself, in memory at $0800. The
relevant code starts at $086A:

*86AL

086A-   A9 00       LDA   #$00
086C-   85 BE       STA   $BE
086E-   A9 7F       LDA   #$7F
0870-   85 BF       STA   $BF
0872-   A9 00       LDA   #$00
0874-   85 BA       STA   $BA
0876-   A9 2B       LDA   #$2B
0878-   85 BB       STA   $BB

; sets some zero page addresses and
; clears the screen via $FC58
087A-   20 68 1E    JSR   $1E68

087D-   A5 BA       LDA   $BA
087F-   85 E4       STA   $E4
0881-   A5 BB       LDA   $BB
0883-   85 E5       STA   $E5
0885-   A9 00       LDA   #$00
0887-   85 E2       STA   $E2
0889-   A9 00       LDA   #$00
088B-   85 E3       STA   $E3
088D-   20 34 1E    JSR   $1E34
0890-   90 03       BCC   $0895

*1E34L

1E34-   A9 01       LDA   #$01
1E36-   20 F3 1D    JSR   $1DF3

*1DF3L

; This appears to be part of an RWTS
; parameter table starting at $1DDA.
; That would mean $1DEA is the RWTS
; command. $01=read
1DF3-   8D EA 1D    STA   $1DEA

; Zero page $E4 was set from zero page
; $BA (at $087F) which was set to #$00
; (at $0874), and it's being stored in
; the RWTS parameter table as the low
; byte of the memory address to store
; the sector data we're about to read.
1DF6-   A5 E4       LDA   $E4
1DF8-   8D E6 1D    STA   $1DE6

; Zero page $E5 was set from zero page
; $BB (at $0883) which was set to #$2B
; (at $0878), and it's being used as
; the high byte of the read address.
1DFB-   A5 E5       LDA   $E5
1DFD-   8D E7 1D    STA   $1DE7

; Starting at track $03, this takes a
; block number in zero page $E2 and
; calculates the actual track/sector
; based on the number of sectors per
; track (in zero page $7F, as I
; correctly guessed earlier).
1E00-   A9 03       LDA   #$03
1E02-   8D E2 1D    STA   $1DE2
1E05-   A5 E2       LDA   $E2
1E07-   A6 E3       LDX   $E3
1E09-   38          SEC
1E0A-   E5 7F       SBC   $7F
1E0C-   B0 04       BCS   $1E12
1E0E-   CA          DEX
1E0F-   30 07       BMI   $1E18
1E11-   38          SEC
1E12-   EE E2 1D    INC   $1DE2
1E15-   4C 0A 1E    JMP   $1E0A
1E18-   18          CLC
1E19-   65 7F       ADC   $7F
1E1B-   8D E3 1D    STA   $1DE3

; call the RWTS
1E1E-   A9 1D       LDA   #$1D
1E20-   A0 DE       LDY   #$DE
1E22-   20 00 29    JSR   $2900
1E25-   60          RTS

So what's the problem? The very first
disk read is into $2B00 -- the last
page of the RWTS itself.

How did this original disk ever work?

Well, funny story about that. It turns
out that, in a 13-sector RWTS, the last
page of code is only used to format a
disk. Since Infocom games never need to
format a disk, and memory was super
tight, they decided to reuse the unused
last page of the RWTS for game data.

In a 16-sector RWTS, the last page of
code is only used to format a disk...
and to hold the sector mapping array
(that 16-byte table that I just patched
in the previous chapter).

Memory was so tight in 1980 that they
literally OVERWROTE THEIR OWN LOW-LEVEL
DISK DRIVER but like just a little bit.
Just the part they weren't using.

But... I need that page. But do I?
Seriously, the *only* thing in that
sector that I'm using is the 16-byte
sector order array. I suppose I could
move that somewhere else and let this
interpreter continue to OVERWRITE ITS
OWN LOW-LEVEL DISK DRIVER which is kind
of a ridiculous thing to do in 2018 but
I guess this is all a little ridiculous
in 2018 so okay let's do this.

Early versions of DOS 3.3 had some
unused memory at $BA69. (This was later
used for OS-level patches.) Looking at
T00,S04, Infocom was not using this for
anything. I can easily fit the 16-byte
sector ordering table there.

T00,S04,$69: ->
  000D0B09070503010E0C0A080604020F

The code that uses the sector ordering
table it is on T00,S08. Since this RWTS
lives at $2400 instead of $B800, the
old address is $2BB8 and the new
address is $2669:

T00,S08,$2C: B82B -> 6926

]PR#6
...works, and it is glorious...

(Brief epilogue: how did the 16-sector
releases of Zork handle this problem?
It turns out that Infocom's interpreter
centrally manages memory allocation, so
they could simply load the first page
of game data into $2C00 instead of
$2B00. No other changes required. I
considered doing this instead of moving
the sector ordering table, but in the
end I decided that it was cleaner to
keep all patches within the RWTS and
leave the interpreter code untouched.)

Quod erat liberandum.

                   ~

            Acknowledgments


Thanks to Ian Baronofsky for lending me
original disk.

Thanks to @brouhaha on Twitter for
helping me understand the interpreter
enough to patch it.

Thanks to qkumba for proposing a
solution that didn't involve patching
the interpreter after all.

---------------------------------------
A 4am crack                    No. 1724
------------------EOF------------------
